// Learn TypeScript:
//  - https://docs.cocos.com/creator/manual/en/scripting/typescript.html
// Learn Attribute:
//  - https://docs.cocos.com/creator/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
//  - https://docs.cocos.com/creator/manual/en/scripting/life-cycle-callbacks.html

export default class BezierCurve {
    start: cc.Vec2 = null;
    end: cc.Vec2 = null;
    startCtrlPoint: cc.Vec2 = null;
    endCtrlPoint: cc.Vec2 = null;
    constructor(start: cc.Vec2, end: cc.Vec2, starCtrlPoint: cc.Vec2, endCtrlPoint: cc.Vec2) {
        this.start = start;
        this.end = end;
        this.startCtrlPoint = starCtrlPoint; // cp0, cp1
        this.endCtrlPoint = endCtrlPoint;   // cp2, cp3
    }

    // Get point at relative position in curve according to arc length
    // - u [0 .. 1]
    public getPointAt = function (u) {
        var t = this.getUtoTmapping(u);
        return this.getPoint(t);
    };

    // Get point at time t
    //  - t [0 .. 1]
    public getPoint = function (t) {
        var x = this.bezier(this.start.x, this.startCtrlPoint.x, this.endCtrlPoint.x, this.end.x, t);
        var y = this.bezier(this.start.y, this.startCtrlPoint.y, this.endCtrlPoint.y, this.end.y, t);

        return cc.v2(x, y);
    };

    // Get total curve arc length
    public getLength = function () {

        var lengths = this.getLengths();
        return lengths[lengths.length - 1];

    };

    // Get list of cumulative segment lengths
    private getLengths = function (divisions) {

        if (!divisions) divisions = (this.__arcLengthDivisions) ? (this.__arcLengthDivisions) : 200;

        if (this.cacheArcLengths
            && (this.cacheArcLengths.length === divisions + 1)) {

            //console.log( "cached", this.cacheArcLengths );
            return this.cacheArcLengths;

        }

        var cache = [];
        var current, last = this.getPoint(0), vector = cc.v2();
        var p, sum = 0;

        cache.push(0);

        for (p = 1; p <= divisions; p++) {

            current = this.getPoint(p / divisions);
            vector.x = last.x - current.x;
            vector.y = last.y - current.y;
            sum += vector.mag();
            cache.push(sum);
            last = current;

        }

        this.cacheArcLengths = cache;

        return cache; // { sums: cache, sum:sum }; Sum is in the last element.
    };

    private getUtoTmapping = function (u, distance) {

        var arcLengths = this.getLengths();

        var i = 0, il = arcLengths.length;

        var targetArcLength; // The targeted u distance value to get

        if (distance) {
            targetArcLength = distance;
        } else {
            targetArcLength = u * arcLengths[il - 1];
        }

        //var time = Date.now();

        // binary search for the index with largest value smaller than target u distance

        var low = 0, high = il - 1, comparison;

        while (low <= high) {

            i = Math.floor(low + (high - low) / 2); // less likely to overflow, though probably not issue here, JS doesn't really have integers, all numbers are floats

            comparison = arcLengths[i] - targetArcLength;

            if (comparison < 0) {

                low = i + 1;
                continue;

            } else if (comparison > 0) {

                high = i - 1;
                continue;

            } else {

                high = i;
                break;

                // DONE

            }

        }

        i = high;

        //console.log('b' , i, low, high, Date.now()- time);

        if (arcLengths[i] === targetArcLength) {

            var t = i / (il - 1);
            return t;

        }

        // we could get finer grain at lengths, or use simple interpolatation between two points

        var lengthBefore = arcLengths[i];
        var lengthAfter = arcLengths[i + 1];

        var segmentLength = lengthAfter - lengthBefore;

        // determine where we are between the 'before' and 'after' points

        var segmentFraction = (targetArcLength - lengthBefore) / segmentLength;

        // add that fractional amount to t

        var t = (i + segmentFraction) / (il - 1);

        return t;
    }

    private bezier(C1, C2, C3, C4, t) {
        var t1 = 1 - t;
        return t1 * (t1 * (C1 + (C2 * 3 - C1) * t) + C3 * 3 * t * t) + C4 * t * t * t;
    }
}
